
public class NeuralNetwork {

    public double learningSetInput[][];
    public double learningSetOutput[][];
    public double testingSetInput[][];
    public double testingSetOutput[][];
    public int  maxIteration = 5000;
    public NeuralLayer layersArray[];
    public int numberOfLayers = 3; // La entrada, la hidden y la salida
    public double currentOutput[][];
    public double learningConst = 0.05;
    public double minError = 0.0;
    public double errorsArray[];
    public NeuralNetwork() {
    
    }
    public NeuralNetwork (int maxIteration, int hiddenNumberOfNeurons , int learningSetSize, int activation) {
        this.maxIteration = maxIteration;
        errorsArray = new double[maxIteration];
        
        layersArray = new NeuralLayer[3]; // capa de entrada, hidden y  de salida
        //La primera capa tiene la misma cantidad de neuronas que de entradas : n        
        layersArray[0] = new NeuralLayer(1, 1, activation);

        //La hidden tiene una cantidad variable de neuronas, pero el numero de entradas es (n-1)        
        layersArray[1] = new NeuralLayer(hiddenNumberOfNeurons, 1, activation);

        //La salida tiene un numero variable de neuroanas pero el numero de entradas es (n-1) o sea al numero de neuronas anterior
        layersArray[2] = new NeuralLayer(1, hiddenNumberOfNeurons, activation);
        currentOutput = new double[learningSetSize][1];
      
    }

    public NeuralNetwork (int maxIteration, int hiddenNumberOfNeurons , int learningSetSize, int activation, int inputSize) {
        this.maxIteration = maxIteration;
        errorsArray = new double[maxIteration];
        
        layersArray = new NeuralLayer[3]; // capa de entrada, hidden y  de salida
        //La primera capa tiene la misma cantidad de neuronas que de entradas : n        
        layersArray[0] = new NeuralLayer(inputSize, inputSize, activation);

        //La hidden tiene una cantidad variable de neuronas, pero el numero de entradas es (n-1)        
        layersArray[1] = new NeuralLayer(hiddenNumberOfNeurons, inputSize, activation);

        //La salida tiene un numero variable de neuroanas pero el numero de entradas es (n-1) o sea al numero de neuronas anterior
        layersArray[2] = new NeuralLayer(1, hiddenNumberOfNeurons, activation);
        currentOutput = new double[learningSetSize][1];
      
    }
    public void testXOR() {

        // Estos array son de 2 dimensiones, la primera el lugar que ocupa en el set. Y la siguente un array que representa los datos de entrada.
        //Este input representa la entrada para XOR

        this.learningSetInput = new double[][]{
            {0.0, 0.0},
            {1.0, 0.0},
            {0.0, 1.0},
            {1.0, 1.0}
        };

        this.learningSetOutput = new double[][]{
            {0.0},
            {1.0},
            {1.0},
            {0.0}};

        currentOutput = new double[learningSetInput.length][2];

        layersArray = new NeuralLayer[3]; // capa de entrada, hidden y  de salida
        //La primera capa tiene la misma cantidad de neuronas que de entradas : n        
        layersArray[0] = new NeuralLayer(2, 2);

        //La hidden tiene una cantidad variable de neuronas, pero el numero de entradas es (n-1)        
        layersArray[1] = new NeuralLayer(6, 2);

        //La salida tiene un numero variable de neuroanas pero el numero de entradas es (n-1) o sea al numero de neuronas anterior
        layersArray[2] = new NeuralLayer(1, 6);



        training();


        //TESTEO en este caso es igual al learing, pero podrian ser distintos
        this.testingSetInput = new double[][]{
            {0.0, 0.0},
            {1.0, 0.0},
            {0.0, 1.0},
            {1.0, 1.0}
        };

        this.testingSetOutput = new double[][]{
            {0.0},
            {1.0},
            {1.0},
            {0.0}};

        int okNumber = 0;
        for (int i = 0; i < testingSetInput.length; i++) {
            System.out.println(" Valor real: " + learningSetOutput[i][0]);
            System.out.println("Valor evaluado: " + evaluate(testingSetInput[i]));
            System.out.println("Diff: " + (evaluate(testingSetInput[i]) - learningSetOutput[i][0]));

            if (testingSetOutput[i][0] == evaluate(testingSetInput[i])) {
                okNumber++;

            }

        }
        System.out.println("numero de aciertos: " + okNumber);
        System.out.println("globalError: " + getGlobalError());

    }



    public void training() {

        int  iterationNumber = 0;


        //Hasta tener un error aceptable, repetir
        do {
            // Para cada ejemplo de entrenamiento (x,t): 
            for (int s = 0; s < learningSetInput.length; s++) {

                for (int i = 0; i < layersArray[0].neuronsArray.length; i++) {
                    layersArray[0].inputArray[i] = learningSetInput[s][i];
                }

                //Propagar entrada hacia delante: dada la entrada x en la red, computar cada salida de cada unidad de la red. 
                FeedForward();
                for (int i = 0; i < layersArray[numberOfLayers - 1].neuronsArray.length; i++) {
                    currentOutput[s][i] = layersArray[numberOfLayers - 1].neuronsArray[i].output;
                }
                //Propagar errores hacia atrás y se calculan los deltaErrors
                backPropagateErrors(s);

                //Actualizar cada peso wji:
                updateWeights();
            }
            errorsArray[iterationNumber] = getGlobalError();
            iterationNumber++;
            

        } while (/*(getGlobalError() > minError) &&*/ (iterationNumber < maxIteration));

    }

    public void backPropagateErrors(int s) {
        int i, j, k, OutputLayer;
        double sum;

        OutputLayer = layersArray.length - 1;

        // Para cada unidad de salida k calcular el error δk:
        for (i = 0; i < layersArray[OutputLayer].neuronsArray.length; i++) {
            double oK = layersArray[OutputLayer].neuronsArray[i].output;
            double tK = learningSetOutput[s][i];

            layersArray[OutputLayer].neuronsArray[i].deltaError = layersArray[OutputLayer].neuronsArray[i].outputDerived*(tK - oK);
        }

        // Para cada unidad oculta, calcular su error δh: 
        for (i = layersArray.length - 2; i > 0; i--) {
            for (j = 0; j < layersArray[i].neuronsArray.length; j++) {
                sum = 0;

                for (k = 0; k < layersArray[i + 1].neuronsArray.length; k++) {
                    sum = sum + layersArray[i + 1].neuronsArray[k].weightArray[j] * layersArray[i + 1].neuronsArray[k].deltaError;
                }

                layersArray[i].neuronsArray[j].deltaError = layersArray[i].neuronsArray[j].outputDerived * sum;
            }
        }

    }

    public void FeedForward() {

        //Pongo la entrada en el output de la capa de entrada
        for (int i = 0; i < layersArray[0].neuronsArray.length; i++) {
            layersArray[0].neuronsArray[i].output = layersArray[0].inputArray[i];
        }
        
        //La entrada de la hidden
        layersArray[1].inputArray = layersArray[0].inputArray;
        for (int i = 1; i < numberOfLayers; i++) {
            layersArray[i].FeedForward();

            //Asigno a la entrada de la capa, la salida de la capa anterior
            if (i != numberOfLayers - 1) {
                layersArray[i + 1].inputArray = layersArray[i].outputArray();
            }
        }

    }

    public void updateWeights() {
        for (int i = numberOfLayers - 1; i > 0; i--) {
            for (int j = 0; j < layersArray[i].neuronsArray.length; j++) {
                for (int k = 0; k < layersArray[i].inputArray.length; k++) {
                    layersArray[i].neuronsArray[j].weightArray[k] = layersArray[i].neuronsArray[j].weightArray[k] + learningConst * layersArray[i].neuronsArray[j].deltaError * layersArray[i - 1].neuronsArray[k].output;
                }
            }
        }

    }

    public double getGlobalError() {
        double globalError = 0;
        for (int i = 0; i < learningSetInput.length; i++) {
            for (int j = 0; j < layersArray[numberOfLayers - 1].numberOfNeurons; j++) {
                globalError = globalError + 0.5 * (Math.pow(learningSetOutput[i][j] - currentOutput[i][j], 2));
            }
        }

        //System.out.println("globalError: " + globalError);
        return globalError;
    }

  
    
    //Dado una entrada evalua la salida de la red neuronal
    public double evaluate(double[] input) {
        int res = 0;
        double[] outputArray;

        for (int j = 0; j < layersArray[0].numberOfNeurons; j++) {
            layersArray[0].inputArray[j] = input[j];
        }

        FeedForward();

        // Obtengo el output de la ultima capa de salida 
        outputArray = (layersArray[numberOfLayers - 1]).outputArray();

        //obtengo el mayor. en este caso la salida es 1 por lo que es innecesario
        for (int k = 0; k < outputArray.length; k++) {
            if (outputArray[res] < outputArray[k]) {
                res = k;
            }
        }

        return outputArray[res];
    }
}
